<?php

namespace App\Http\Controllers;

use App\Commands\CreateVulnerability;
use App\Commands\DeleteVulnerability;
use App\Commands\EditVulnerability;
use App\Commands\GetAssets;
use App\Commands\GetVulnerability;
use App\Commands\GetWorkspaceApp;
use App\Entities\Asset;
use App\Entities\Folder;
use App\Entities\Vulnerability;
use App\Policies\ComponentPolicy;
use Auth;
use Doctrine\Common\Collections\Collection;
use Illuminate\Support\Collection as LaravelCollection;

/**
 * @Middleware({"web", "auth"})
 */
class VulnerabilityController extends AbstractController
{
    /** Invalid image error message template */
    const VALIDATION_ERROR_THUMBNAIL = 'It seems like the file you selected for "%s" is not an valid image. '
    . 'Valid image types are: jpeg, png, bmp, gif, or svg.';

    /**
     * Get a single Vulnerability record related to a specific file
     *
     * @GET("/vulnerability/{vulnerabilityID}", as="vulnerability.view", where={"vulnerabilityId":"[0-9]+"})
     *
     * @param $vulnerabilityId
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function appShowRecord($vulnerabilityId)
    {
        $command           = new GetVulnerability(intval($vulnerabilityId));
        $vulnerabilityInfo = $this->sendCommandToBusHelper($command);

        if ($this->isCommandError($vulnerabilityInfo)) {
            return redirect()->back();
        }

        /** @var Vulnerability $vulnerability */
        $vulnerability = $vulnerabilityInfo->get('vulnerability');

        return view('workspaces.appShowRecord', [
            'vulnerability'      => $vulnerabilityInfo->get('vulnerability'),
            'assets'             => $vulnerabilityInfo->get('assets'),
            'httpDataCollection' => $vulnerabilityInfo->get('httpData'),
            'comments'           => $vulnerabilityInfo->get('comments'),
            'folders'            => $this->getFoldersForSelect(
                $vulnerability->getFile()->getWorkspaceApp()->getWorkspace()->getFolders()
            ),
        ]);
    }

    /**
     * Show the form to create a new custom Vulnerability in the Ruggedy App
     *
     * @GET("/workspace/ruggedy-app/create/{workspaceAppId}", as="vulnerability.create",
     *     where={"workspaceAppId":"[0-9]+"})
     *
     * @param $workspaceAppId
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\View\View
     */
    public function ruggedyCreateVulnerability($workspaceAppId) {
        $command          = new GetWorkspaceApp(intval($workspaceAppId));
        $workspaceAppInfo = $this->sendCommandToBusHelper($command);

        if ($this->isCommandError($workspaceAppInfo)) {
            return redirect()->back();
        }

        if (Auth::user()->cannot(ComponentPolicy::ACTION_CREATE, $workspaceAppInfo->get('app'))) {
            $this->flashMessenger->error("You do not have permission to create Vulnerabilities in that App.");
            return redirect()->back();
        }

        return view('workspaces.ruggedy-create', [
            'workspaceApp' => $workspaceAppInfo->get('app'),
            'file'         => $workspaceAppInfo->get('files')->first(),
            'severities'   => $this->generateSeveritiesForSelect(),
            'vendors'      => $this->generateVendorsForSelect(),
            'assetIds'     => $this->generateAssetIdsForSelect(),
            'assets'       => $this->getRelatedAssets(),
        ]);
    }

    /**
     * Store a new custom Vulnerability for the Ruggedy App
     *
     * @POST("/workspace/ruggedy-app/store/{fileId}", as="vulnerability.store",
     *     where={"fileId":"[0-9]+"})
     *
     * @param $fileId
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\View\View
     */
    public function ruggedyStoreVulnerability($fileId)
    {
        // Validate the form submission
        $this->validate($this->request, $this->getValidationRules(), $this->getValidationMessages());

        // Create a new Vulnerability and populate from the request
        $vulnerability = new Vulnerability();
        $vulnerability->setName($this->request->get('name'))
            ->setDescription($this->request->get('description'))
            ->setSolution($this->request->get('solution'))
            ->setPoc($this->request->get('poc'))
            ->setSeverity($this->request->get('severity'))
            ->setCvssScore($this->request->get('cvss_score'))
            ->setThumbnail1($this->request->file('thumbnail_1'))
            ->setThumbnail2($this->request->file('thumbnail_2'))
            ->setThumbnail3($this->request->file('thumbnail_3'));

        $command = new CreateVulnerability(intval($fileId), $vulnerability, $this->request->get('assets', []));
        $vulnerability = $this->sendCommandToBusHelper($command);
        if ($this->isCommandError($vulnerability)) {
            return redirect()->back()->withInput();
        }

        $this->flashMessenger->success("A new custom Vulnerability has been created in your Ruggedy App.");
        return redirect()->route('ruggedy-app.view', [
            $vulnerability->getFile()->getRouteParameterName() => $vulnerability->getFile()->getId()
        ]);

    }

    /**
     * Show the form to edit a Vulnerability
     *
     * @GET("/ruggedy-app/edit/{vulnerabilityId}", as="vulnerability.edit", where={"vulnerabilityId":"[0-9]+"})
     *
     * @param $vulnerabilityId
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\View\View
     */
    public function ruggedyEditVulnerability($vulnerabilityId)
    {
        $command = new GetVulnerability(intval($vulnerabilityId));
        /** @var Vulnerability $vulnerability */
        $vulnerabilityInfo = $this->sendCommandToBusHelper($command);

        if ($this->isCommandError($vulnerabilityInfo)) {
            return redirect()->back();
        }

        $vulnerability = $vulnerabilityInfo->get('vulnerability');

        return view('workspaces.ruggedy-edit', [
            'workspaceApp'  => $vulnerability->getFile()->getWorkspaceApp(),
            'vulnerability' => $vulnerability,
            'file'          => $vulnerability->getFile(),
            'severities'    => $this->generateSeveritiesForSelect(),
            'vendors'       => $this->generateVendorsForSelect(),
            'assetIds'      => $this->generateAssetIdsForSelect($vulnerability),
            'assets'        => $this->getRelatedAssets($vulnerability),
        ]);
    }

    /**
     * Update a custom Vulnerability entry
     *
     * @POST("/ruggedy-app/update/{vulnerabilityId}", as="vulnerability.update", where={"vulnerabilityId":"[0-9]+"})
     *
     * @param $vulnerabilityId
     * @return \Illuminate\Http\RedirectResponse
     */
    public function ruggedyUpdateVulnerability($vulnerabilityId)
    {
        // Validate the form submission
        $this->validate($this->request, $this->getValidationRules(), $this->getValidationMessages());

        $requestInfo = $this->updateThumbnailHelper($this->request->all());
        if (isset($requestInfo['assets'])) {
            unset($requestInfo['assets']);
        }

        // Instantiate the EditVulnerability command
        $command = new EditVulnerability(
            intval($vulnerabilityId),
            $requestInfo,
            $this->request->get('assets', [])
        );

        $vulnerability = $this->sendCommandToBusHelper($command);
        if ($this->isCommandError($vulnerability)) {
            return redirect()->back()->withInput();
        }

        $this->flashMessenger->success("Vulnerability information updated successfully.");
        return redirect()->route('vulnerability.view', [
            $vulnerability->getRouteParameterName() => $vulnerability->getId()
        ]);
    }

    /**
     * Delete a custom Vulnerability entry
     *
     * @GET("/ruggedy-app/delete/{vulnerabilityId}", as="vulnerability.delete", where={"vulnerabilityId":"[0-9]+"})
     *
     * @param $vulnerabilityId
     * @return \Illuminate\Http\RedirectResponse
     */
    public function ruggedyDeleteVulnerability($vulnerabilityId)
    {
        $command = new DeleteVulnerability(intval($vulnerabilityId), true);
        /** @var Vulnerability $vulnerability */
        $vulnerability = $this->sendCommandToBusHelper($command);
        if ($this->isCommandError($vulnerability)) {
            return redirect()->back();
        }

        $this->flashMessenger->success("Vulnerability record deleted successfully.");
        return redirect()->route('ruggedy-app.view', [
            'fileId' => $vulnerability->getFileId()
        ]);
    }

    /**
     * Convert an ArrayCollection of Folder entities into an array of Folder names indexed by the Folder ID
     *
     * @param Collection $folders
     * @return array
     */
    protected function getFoldersForSelect(Collection $folders): array
    {
        return collect($folders->toArray())->reduce(function ($foldersForSelect, $folder) {
            /** @var Folder $folder */
            $foldersForSelect[$folder->getId()] = $folder->getName();
            return $foldersForSelect;
        }, []);
    }

    /**
     * Get an array of severities formatted for a HTML Collective select input
     *
     * @return array
     */
    protected function generateSeveritiesForSelect(): array
    {
        // An array of severities for the Vulnerability severity select input
        return Vulnerability::getSeverityTextToScoreMap()->map(function($score) {
            return intval($score);
        })->flip()
          ->prepend("-- Select a Severity --", "")
          ->toArray();
    }

    /**
     * Get an array of OS vendors formatted for a HTML Collective select input
     *
     * @return array
     */
    protected function generateVendorsForSelect(): array
    {
        // An array of valid Asset OS vendors for the select input
        return Asset::getValidOsVendors()->reduce(function ($vendors, $vendor) {
            /** @var \Illuminate\Support\Collection $vendors */
            return $vendors->put($vendor, $vendor);
        }, collect([]))
            ->prepend(Asset::OS_VENDOR_UNKNOWN, Asset::OS_VENDOR_UNKNOWN)
            ->prepend("-- Select a Vendor --", "")
            ->toArray();
    }

    /**
     * Generate the HTML to display the Asset blocks on the create/edit Vulnerability view
     *
     * @param Vulnerability $vulnerability
     * @return \Illuminate\Support\Collection
     */
    protected function getRelatedAssets(Vulnerability $vulnerability = null): LaravelCollection
    {
        // Make sure we have some Asset IDs
        $assetIds = $this->generateAssetIdsForSelect($vulnerability);
        if (empty($assetIds)) {
            return collect();
        }

        // Get all the related Assets
        $getAssets = new GetAssets($assetIds);
        $assets    = $this->sendCommandToBusHelper($getAssets);

        // Make sure the command was successful
        if ($this->isCommandError($assets)) {
            return collect();
        }

        // Generate and retun the Asset HTML
        return collect($assets);
    }

    /**
     * Convert the numerically indexed sequential array of asset IDs into an array with asset ID as both key and value
     *
     * @param Vulnerability $vulnerability
     * @return array
     */
    protected function generateAssetIdsForSelect(Vulnerability $vulnerability = null): array
    {
        // If there are no Asset IDs in the request and no Vulnerability given return an empty array
        $assetIds = $this->request->old('assets', []);
        if (empty($assetIds) && empty($vulnerability)) {
            return [];
        }

        // If there are not Asset IDs in the request, but we were supplied a
        // Vulnerability entity, map an array of Asset IDs only
        if (empty($assetIds) && !empty($vulnerability)) {
            $assetIds = $vulnerability->getAssets()->map(function ($asset) {
                /** @var Asset $asset */
                return $asset->getId();
            })->toArray();
        }

        // Combine the array to itself so keys and values are both the relevant Asset ID value
        return array_combine($assetIds, $assetIds);
    }

    /**
     * Helper to set thumbnail_x to either the image path, or the UploadedFile value for new files
     *
     * @param array $requestInfo
     * @return array
     */
    protected function updateThumbnailHelper(array $requestInfo)
    {
        $requestInfo['thumbnail_1'] = $this->request->get('thumbnail_1_path') ?? $this->request->file('thumbnail_1');
        $requestInfo['thumbnail_2'] = $this->request->get('thumbnail_2_path') ?? $this->request->file('thumbnail_2');
        $requestInfo['thumbnail_3'] = $this->request->get('thumbnail_3_path') ?? $this->request->file('thumbnail_3');

        return $requestInfo;
    }

    /**
     * @inheritdoc
     *
     * @return array
     */
    protected function getValidationRules(): array
    {
        return [
            'name'        => 'bail|required',
            'severity'    => 'bail|nullable|in:0,3,6,9,10',
            'cvss_score'  => 'bail|nullable|numeric|between:0,10',
            'thumbnail_1' => 'bail|nullable|image',
            'thumbnail_2' => 'bail|nullable|image',
            'thumbnail_3' => 'bail|nullable|image',
        ];
    }

    /**
     * @inheritdoc
     *
     * @return array
     */
    protected function getValidationMessages(): array
    {
        return [
            'name.required'      => 'A Vulnerability name is required but it does not seem like you entered one. '
                . 'Please try again.',
            'severity.in'        => 'Please select a valid risk score (severity).',
            'cvss_score.numeric' => 'CVSS score must be a number and it does not seem like you entered a number. '
                . 'Please try again',
            'cvss_score.between' => 'CVSS score must be a number between 0 and 10 and it seems like you entered a '
                . 'number outside of that range. Please try again.',
            'thumbnail_1.image'  => sprintf(self::VALIDATION_ERROR_THUMBNAIL, 'Screenshot 1'),
            'thumbnail_2.image'  => sprintf(self::VALIDATION_ERROR_THUMBNAIL, 'Screenshot 2'),
            'thumbnail_3.image'  => sprintf(self::VALIDATION_ERROR_THUMBNAIL, 'Screenshot 3'),
        ];
    }
}